#include <app.hpp>

#include <string>
#include <vector>
#include <random>
#include <ctime>

#define LINKUP_MAP_WIDTH 14
#define LINKUP_MAP_HEIGHT 10
#define LINKUP_MAP_ITEM_WIDTH 50
#define LINKUP_MAP_ITEM_HEIGHT 50
#define LINKUP_ICON_WIDTH 48
#define LINKUP_ICON_HEIGHT 48

#define LINKUP_ID_CHECK_LINK (APP_USER_EVENT_ID+1)
#define LINKUP_ID_CHECK_GAMEOVER (APP_USER_EVENT_ID+2)

enum class eLinkupState : cvs::uint8 {
  Start,Runing,Finish
};

class Linkup : public App {
  protected:
    struct Point { int x; int y; };
  public:
    Linkup() : last_click_id_(-1), cur_click_id_(-1) {
      mask_ = cvs::Color(0.f,0.f,0.f,0.4f);
      border_ = cvs::Color(0.f, 1.f, 0.f);
      link_ = cvs::Color(1.f, 1.f, 0.f);
      link_width_ = 3.f;
      timer_link_ = 0;
      timer_toplay_ = 0;
      state_ = eLinkupState::Start;
      timer_time_ = 0;

      music_link_ok_ = 0;
      music_link_err_ = 0;

      level_ = 1;

      ctrl_tip_ = 0;
      tip_times_ = 3;
    }
    virtual ~Linkup() {}
  protected:
    virtual cvs::Result create_resource() override {
      auto rst = render_->create_shape_rect(cvs::Rect(), cvs::Size(), &shape_rect_);
      if (cvs::Succeeded(rst))
        rst = render_->create_shape_polyline(0, 0, &shape_polyline_);
      music_link_ok_ = load_music("sound/ok.mp3");
      music_link_err_ = load_music("sound/warning.mp3");
      return rst;
    }
    virtual void release_resource() override {
      cvs::SaveDecref(shape_rect_); shape_rect_ = 0;
      cvs::SaveDecref(shape_polyline_); shape_polyline_ = 0;
      unload_music(music_link_err_);
      unload_music(music_link_ok_);
    }
    virtual cvs::Result create_target_resource() override {
      auto rst = load_icons();
      if (cvs::Succeeded(rst))
        rst = target_->create_solid_color_brush(cvs::Color(), &brush_);
      if (cvs::Succeeded(rst))
        rst = load_ctrls();
      return rst;
    }
    virtual void release_target_resource() override {
      unload_ctrls();
      release_icons();
      cvs::SaveDecref(brush_); brush_ = 0;
    }
    cvs::Result load_ctrls() {
      auto size = window_size();
      cvs::dtype w = 100.f;
      cvs::dtype h = 30.f;
      cvs::dtype x = size.w / 2.f - w / 2.f;
      cvs::dtype y = 0.f;
      cvs::Rect rc(x, y, w, h);

      ctrl_tip_ = new AppControl(this);
      if (ctrl_tip_) {
        ctrl_tip_->set_rect(rc);
        ctrl_tip_->set_radius(cvs::Size(5.f, 5.f));
        ctrl_tip_->set_use_back_color(true);
        ctrl_tip_->set_text(L"Tip");
        ctrl_tip_->normal.back_color = cvs::Color(1.f,0.66f,0.14f);
        ctrl_tip_->normal.text_color = cvs::Color(1.f,1.f,0.96f,0.9f);
        ctrl_tip_->normal.font_format.size = 20.f;
        ctrl_tip_->normal.text_format.halign = cvs::c2d::TextAlignHor::Center;
        ctrl_tip_->normal.text_format.valign = cvs::c2d::TextAlignVer::Middle;
        ctrl_tip_->hot.back_color = cvs::Color(1.f,0.59f,0.f);
        ctrl_tip_->hot.border_color = cvs::Color(0.f,1.f,0.f);
        ctrl_tip_->hot.text_color = cvs::Color(1.f,1.f,0.96f,0.9f);
        ctrl_tip_->hot.font_format.size = 20.f;
        ctrl_tip_->hot.text_format.halign = cvs::c2d::TextAlignHor::Center;
        ctrl_tip_->hot.text_format.valign = cvs::c2d::TextAlignVer::Middle;
        ctrl_tip_->pushed.back_color = cvs::Color(95.f,0.5f,0.02f);
        ctrl_tip_->pushed.text_color = cvs::Color(1.f,1.f,0.96f,0.9f);
        ctrl_tip_->pushed.font_format.size = 20.f;
        ctrl_tip_->pushed.text_format.halign = cvs::c2d::TextAlignHor::Center;
        ctrl_tip_->pushed.text_format.valign = cvs::c2d::TextAlignVer::Middle;
        ctrl_tip_->set_callback(std::bind(&Linkup::on_ctrl_tip, this, std::placeholders::_1));
        ctrl_tip_->set_visible(false);
      }
      return CVS_S_Ok;
    }
    void unload_ctrls() {
      if (ctrl_tip_) {
        delete ctrl_tip_;
        ctrl_tip_ = 0;
      }
    }
  protected:
    virtual void on_draw() override {
      cvs::Color bkcolor(0.7f, 0.7f, 0.7f);
      target_->clear(&bkcolor);
      //draw_fps();
      switch (state_) {
        case eLinkupState::Start:
          draw_start();
          break;
        case eLinkupState::Runing:
          draw_runing();
          break;
        case eLinkupState::Finish:
          draw_finish();
          break;
      }
    }
    virtual void on_mouse_click(const SDL_MouseButtonEvent &evt) override {
      if (evt.state == SDL_PRESSED && evt.button == SDL_BUTTON_RIGHT) {
        if (state_ == eLinkupState::Runing) {
          last_click_id_ = -1;
          invaldate();
        }
      } else if (evt.state == SDL_PRESSED && evt.button == SDL_BUTTON_LEFT) {
        auto size = window_size();
        if (state_ == eLinkupState::Start) {
          cvs::dtype w = 100.f;
          cvs::dtype h = 50.f;
          cvs::dtype x = size.w / 2.f - w / 2.f;
          cvs::dtype y = size.h / 2.f - h / 2.f;
          cvs::Rect rc(x, y, w, h);
          if (evt.x > rc.l() && evt.x < rc.r()
                && evt.y > rc.t() && evt.y < rc.b()) {
            gen_map();
            state_ = eLinkupState::Runing;
            time_begin_ = SDL_GetTicks();
            time_end_ = SDL_GetTicks();
            timer_time_ = add_timer(1000);
            //tip_times_ = 3;
            ctrl_tip_->set_visible(true);
            tip_pt1_.x = -1;
            tip_pt2_.x = -1;
            invaldate();
          }
        } else if (state_ == eLinkupState::Runing) {
          cvs::dtype x=0.f, y=0.f;
          x = (size.w-LINKUP_MAP_ITEM_WIDTH*LINKUP_MAP_WIDTH)/2.f;
          y = (size.h-LINKUP_MAP_ITEM_HEIGHT*LINKUP_MAP_HEIGHT)/2.f;
          if (evt.x < x || evt.y < y) return;
          int i = (evt.x - x) / LINKUP_MAP_ITEM_WIDTH;
          int j = (evt.y - y) / LINKUP_MAP_ITEM_HEIGHT;
          if (i >= LINKUP_MAP_WIDTH || j >= LINKUP_MAP_HEIGHT) return;
          int id = j * LINKUP_MAP_WIDTH + i;
          action(id);
        }
      }
    }
    virtual void on_user(Sint32 id, void *data1, void *data2) override {
      if (id == LINKUP_ID_CHECK_LINK) {
        on_check_link();
      } else if (id == LINKUP_ID_CHECK_GAMEOVER) {
        if (gameover()) {
          state_ = eLinkupState::Finish;
          timer_toplay_ = add_timer(3000);
          remove_timer(timer_time_);
          ++level_;
          ctrl_tip_->set_visible(false);
        }
        invaldate();
      }
    }
    virtual void on_timer(SDL_TimerID id, Uint32 interval, void *param) override {
      if (id == timer_link_) {
        remove_timer(timer_link_);
        timer_link_ = 0;
        on_linked();
      } else if (id == timer_toplay_) {
        remove_timer(timer_toplay_);
        timer_toplay_ = 0;
        state_ = eLinkupState::Start;
        invaldate();
      } else if (id == timer_time_) {
        time_end_ = SDL_GetTicks();
        invaldate();
      }
    }
  protected:
    cvs::Result load_icons() {
      release_icons();

      static const char *files[] = {
        "image/chilli.png",
        "image/cholesterol.png",
        "image/cholesterol-2.png",
        "image/cucumber.png",
        "image/carrot.png",
        "image/artichoke.png",
        "image/egg-plant.png",
        "image/leek.png",
        "image/mushroom.png",
        "image/peas.png",
        "image/potato.png",
        "image/pumpkin.png",
        "image/radish.png",
        "image/tomato.png",
        "image/turnip.png",
        //
        "linkup/babytchi.png",
        "linkup/gozarutchi.png",
        "linkup/hanatchi.png",
        "linkup/hashizotchi.png",
        "linkup/hinotamatchi.png",
        "linkup/ichigotchi.png",
        "linkup/kinakomotchi.png",
        "linkup/kuchipatchi.png",
        "linkup/kuchipatchi1.png",
        "linkup/kusatchi.png",
        "linkup/mametchi.png",
        "linkup/mametchi1.png",
        "linkup/mametchi2.png",
        "linkup/mametchi3.png",
        "linkup/mametchi4.png",
        "linkup/maretchi.png",
        "linkup/marutchi.png",
        "linkup/masktchi.png",
        "linkup/memetchi.png",
        "linkup/mimitchi.png",
        "linkup/mimitchi1.png",
        "linkup/oniontchi.png",
        "linkup/robotchi.png",
        "linkup/tamaburger.png",
        "linkup/tamadog.png",
        "linkup/tamagotchi.png",
        "linkup/tamkero.png",
        "linkup/tamkero2.png",
        "linkup/tamkero3.png",
        "linkup/tamkero4.png",
        "linkup/tarakotchi.png",
        "linkup/tarakotchi1.png",
        "linkup/tarakotchi2.png",
        "linkup/uglytama.png",
        "linkup/young mimitchi.png",
        "linkup/zukitchi.png",
      };

      cvs::c2d::ISurface *surface = 0;
      cvs::c2d::IBitmap *bitmap = 0;
      for (cvs::uint i = 0; i < CVS_ArraySize(files); ++i) {
        surface = 0;
        bitmap = 0;
        auto rst = render_->create_surface(&surface);
        if (cvs::Succeeded(rst))
          rst = surface->load_file(files[i]);
        if (cvs::Succeeded(rst))
          rst = target_->create_bitmap(&bitmap);
        if (cvs::Succeeded(rst))
          rst = bitmap->create(surface);
        if (cvs::Succeeded(rst)) {
          icons_.push_back(bitmap);
          bitmap->incref();
        }
        cvs::SaveDecref(surface);
        cvs::SaveDecref(bitmap);
      }
      
      return CVS_S_Ok;
    }
    void release_icons() {
      for (auto b : icons_)
        cvs::SaveDecref(b);
      icons_.clear();
    }
    void draw_runing() {
      auto size = window_size();
      cvs::dtype x=0.f, y=0.f;
      x = (size.w-LINKUP_MAP_ITEM_WIDTH*LINKUP_MAP_WIDTH)/2.f;
      y = (size.h-LINKUP_MAP_ITEM_HEIGHT*LINKUP_MAP_HEIGHT)/2.f;

      cvs::Point pt;
      cvs::Size radius(5.f, 5.f);
      cvs::Rect rc(0.f, 0.f, LINKUP_MAP_ITEM_WIDTH, LINKUP_MAP_ITEM_HEIGHT);
      cvs::Size offset((rc.w-LINKUP_ICON_WIDTH)/2.f, (rc.h-LINKUP_ICON_HEIGHT)/2.f);
      for (int j = 0; j < LINKUP_MAP_HEIGHT; ++j) {
        for (int i = 0; i < LINKUP_MAP_WIDTH; ++i) {
          auto d = maps_[j][i];
          if (!d_in_map(d))
            continue;
          pt.x = x + i * LINKUP_MAP_ITEM_WIDTH;
          pt.y = y + j * LINKUP_MAP_ITEM_HEIGHT;
          rc.x = pt.x; rc.y = pt.y;
          
          pt.x += offset.w;
          pt.y += offset.h;

          shape_rect_->set(rc, radius);

          target_->draw_bitmap(icons_[d], pt);

          auto id = j * LINKUP_MAP_WIDTH + i;
          if (id == last_click_id_ || id == cur_click_id_
              || (i == tip_pt1_.x && j == tip_pt1_.y)
              || (i == tip_pt2_.x && j == tip_pt2_.y)) {
            brush_->set_color(mask_);
            target_->fill_shape(shape_rect_, brush_);
          }

          brush_->set_color(border_);
          target_->draw_shape(shape_rect_, brush_);
        }
      }

      draw_links(x, y);
      draw_time();
    }
    void draw_links(cvs::dtype x, cvs::dtype y) {
      if (pts_.size() <= 0) return;

      auto pts = new cvs::Point[pts_.size()];
      int i = 0;
      for (auto &p : pts_) {
        pts[i].x = x + pts_[i].x * LINKUP_MAP_ITEM_WIDTH + LINKUP_MAP_ITEM_WIDTH / 2.f;
        pts[i].y = y + pts_[i].y * LINKUP_MAP_ITEM_HEIGHT + LINKUP_MAP_ITEM_HEIGHT / 2.f;
        ++i;
      }
      shape_polyline_->set(pts, pts_.size());
      delete pts;

      brush_->set_color(link_);
      target_->draw_shape(shape_polyline_, brush_, link_width_);
    }
    void draw_time() {
      auto time = time_end_ - time_begin_;

      std::wstring text;
      text = L"Time: ";
      text += std::to_wstring(time/1000);

      auto rc = window_rect();
      rc.x = (rc.w-LINKUP_MAP_ITEM_WIDTH*LINKUP_MAP_WIDTH)/2.f;
      rc.w = LINKUP_MAP_ITEM_WIDTH*LINKUP_MAP_WIDTH;

      cvs::c2d::FontFormat font;
      cvs::c2d::TextFormat format;

      font.family = L"";
      font.size = 20.f;
      font.bold = false;
      font.italic = false;
      font.underline = false;
      font.strikethrough = false;
      format.halign = cvs::c2d::TextAlignHor::Right;
      format.valign = cvs::c2d::TextAlignVer::Middle;
      format.direction = cvs::c2d::TextDirection::LeftToRight;
      format.trimming = cvs::c2d::TextTrimming::None;
      format.word_wrap = false;
      format.auto_width = false;
      format.auto_height = true;

      draw_text(text, rc, cvs::Color(1.f, 0.f, 0.f), font, format);

      
      text = L"Level: ";
      text += std::to_wstring(level_);
      format.halign = cvs::c2d::TextAlignHor::Left;
      draw_text(text, rc, cvs::Color(1.f, 0.f, 0.f), font, format);

      text = L"Tip: ";
      text += std::to_wstring(tip_times_);
      format.halign = cvs::c2d::TextAlignHor::Left;
      rc.x += 100.f;
      draw_text(text, rc, cvs::Color(1.f, 0.f, 0.f), font, format);
    }
    void draw_start() {
      auto size = window_size();
      cvs::dtype w = 100.f;
      cvs::dtype h = 50.f;
      cvs::dtype x = size.w / 2.f - w / 2.f;
      cvs::dtype y = size.h / 2.f - h / 2.f;
      cvs::Rect rc(x, y, w, h);
      shape_rect_->set(rc, cvs::Size(5.f, 5.f));
      brush_->set_color(link_);
      target_->fill_shape(shape_rect_, brush_);
      brush_->set_color(border_);
      target_->draw_shape(shape_rect_, brush_);
      draw_text(L"Play", rc, 20.f, border_);
    }
    void draw_finish() {
      draw_time();
      draw_text(L"GAME OVER!", window_rect(), 40.f, border_);
    }
  protected:
    int get_level_max() {
      int max = icons_.size();
      int count = 10 + level_ * 5;
      if (count > max) count = max;
      return count;
    }
    void gen_map() {
      last_click_id_ = -1;
      cur_click_id_ = -1;

      auto fun_random_last = [this](std::vector<int> &datas) -> int {
        auto d = random(0, datas.size()-1);
        
        int i = 0;
        for (auto it = datas.begin(); it != datas.end(); ++it, ++i) {
          if (i == d) {
            auto d = (*it);
            datas.erase(it);
            return d;
          }
        }
        
        return -1;
      };
      
      std::vector<int> tds;
      int count = LINKUP_MAP_HEIGHT * LINKUP_MAP_WIDTH;
      int add_count = 0;
      for (int i = 0; i < LINKUP_MAP_HEIGHT; ++i) {
        for (int j = 0; j < LINKUP_MAP_WIDTH; ++j) {
          if (add_count < count/2) {
            auto d = random(0, get_level_max() - 1);
            maps_[i][j] = d;
            tds.push_back(d);
          } else {
            maps_[i][j] = fun_random_last(tds);
          }
          ++add_count;
        }
      }
    }
    void sort_map() {
      std::vector<int> ds;
      for (int j = 0; j < LINKUP_MAP_HEIGHT; ++j) {
        for (int i = 0; i < LINKUP_MAP_WIDTH; ++i) {
          if (d_in_map(maps_[j][i])) {
            ds.push_back(maps_[j][i]);
          }
          maps_[j][i] = -1;
        }
      }
      if (ds.size() <= 0) return;

      auto fun_set_idx = [this](int idx, int d) {
        int count = 0;
        for (int j = 0; j < LINKUP_MAP_HEIGHT; ++j) {
          for (int i = 0; i < LINKUP_MAP_WIDTH; ++i) {
            if (d_in_map(maps_[j][i])) continue;
            if (count == idx) {
              maps_[j][i] = d;
              return;
            }
            ++count;
          }
        }
      };

      int max = LINKUP_MAP_HEIGHT * LINKUP_MAP_WIDTH;
      for (auto &d : ds) {
        auto idx = this->random(0, --max);
        fun_set_idx(idx, d);
      }
    }
    bool id_in_map(int id) {
      return (id >= 0 && id < LINKUP_MAP_WIDTH * LINKUP_MAP_HEIGHT);
    }
    bool id_in_map(int x, int y) {
      if (x < 0 || x >= LINKUP_MAP_WIDTH) return false;
      if (y < 0 || y >= LINKUP_MAP_HEIGHT) return false;
      return true;
    }
    bool d_in_map(int d) {
      if (d < 0 || d >= get_level_max()) return false;
      return true;
    }
    bool id_d_in_map(int id) {
      int x = id % LINKUP_MAP_WIDTH;
      int y = id / LINKUP_MAP_WIDTH;
      if (id_in_map(x, y))
        return d_in_map(maps_[y][x]);
      return false;
    }
    void auto_test(const Point &pt1, const Point &pt2) {
      int idx1 = pt1.x + pt1.y * LINKUP_MAP_WIDTH;
      int idx2 = pt2.x + pt2.y * LINKUP_MAP_WIDTH;
      last_click_id_ = -1;
      action(idx1);
      action(idx2);
      invaldate();
    }
    bool gameover() {
      for (int i = 0; i < LINKUP_MAP_HEIGHT; ++i) {
        for (int j = 0; j < LINKUP_MAP_WIDTH; ++j) {
          if (d_in_map(maps_[i][j])) {
            Point pt1, pt2;
            if (!get_one_link(&pt1, &pt2)) {
              sort_map();
              return gameover();
            }
            #ifdef LINKUP_AUTOTEST
              auto_test(pt1, pt2);
            #endif
            return false;
          }
        }
      }
      ++tip_times_;
      return true;
    }
    bool get_one_link(int x, int y, Point *pt) {
      if (!pt) return false;
      if (x < 0 || x > LINKUP_MAP_WIDTH-1) return false;
      if (y < 0 || y > LINKUP_MAP_HEIGHT-1) return false;
      Point pt1;
      pt1.x = x; pt1.y = y;
      Point tmp1, tmp2;
      for (int j = 0; j < LINKUP_MAP_HEIGHT; ++j) {
        for (int i = 0; i < LINKUP_MAP_WIDTH; ++i) {
          if (x == i && y == j) continue;
          if (maps_[j][i] != maps_[y][x]) continue;
          pt->x = i; pt->y = j;
          if (had_0_link(pt1, *pt)
              || had_1_link(pt1, *pt, &tmp1)
              || had_2_link(pt1, *pt, &tmp1, &tmp2)) {
            return true;
          }
        }
      }
      return false;
    }
    bool get_one_link(Point *pt1, Point *pt2) {
      if (!pt1 || !pt2) return false;
      for (int j = 0; j < LINKUP_MAP_HEIGHT; ++j) {
        for (int i = 0; i < LINKUP_MAP_WIDTH; ++i) {
          if (d_in_map(maps_[j][i])) {
            if (get_one_link(i, j, pt2)) {
              pt1->x = i; pt1->y = j;
              return true;
            }
          }
        }
      }
      return false;
    }
    void action(int id) {
      if (!id_in_map(id)) return;
      if (!id_d_in_map(id)) return;
      if (!id_in_map(last_click_id_)) {
        last_click_id_ = id;
        invaldate();
        return;
      }
      if (last_click_id_ == id) return;
      cur_click_id_ = id;
      invaldate();
      do_user_event(LINKUP_ID_CHECK_LINK);
    }
    bool had_0_link(const Point &p1, const Point &p2) {
      if (p1.x == p2.x && p1.y == p2.y) return false;

      if (p1.x != p2.x && p1.y != p2.y)
        return false;
      int min, max;
      if (p1.x == p2.x) {
        min = p1.y < p2.y ? p1.y : p2.y;
        max = p1.y > p2.y ? p1.y : p2.y;
        for(++min; min<max; ++min) {
          if(d_in_map(maps_[min][p1.x]))
            return false;
        }
      } else {
        min = p1.x < p2.x ? p1.x : p2.x;
        max = p1.x > p2.x ? p1.x : p2.x;
        for(++min; min<max; ++min) {
          if (d_in_map(maps_[p1.y][min]))
            return false;
        }
      }
      return true;
    }
    bool had_1_link(const Point &p1, const Point &p2, Point *o) {
      if (p1.x == p2.x || p1.y == p2.y)
        return false;
      
      Point c1,c2;
      c1.x=p1.x;
      c1.y=p2.y;
      c2.x=p2.x;
      c2.y=p1.y;
      if (!d_in_map(maps_[c1.y][c1.x])){
        bool b1 = had_0_link(p1, c1);
        bool b2 = had_0_link(c1, p2);
        if (b1 && b2) {
          *o = c1;
          return true;
        }
      }
      if (!d_in_map(maps_[c2.y][c2.x])){
        bool b1 = had_0_link(p1, c2);
        bool b2 = had_0_link(c2, p2);
        if (b1 && b2) {
          *o = c2;
          return true;
        }
      }
      return false;
    }
    bool had_2_link(const Point &p1, const Point &p2, Point *o1, Point *o2) {
      // 
      {
        Point pt1, pt2;

        pt1.x = -1; pt1.y = p1.y;
        pt2.x = -1; pt2.y = p2.y;
        if (had_0_link(p1, pt1) && had_0_link(p2, pt2)) {
          *o1 = pt1; *o2 = pt2;
          return true;
        }

        pt1.x = LINKUP_MAP_WIDTH; pt1.y = p1.y;
        pt2.x = LINKUP_MAP_WIDTH; pt2.y = p2.y;
        if (had_0_link(p1, pt1) && had_0_link(p2, pt2)) {
          *o1 = pt1; *o2 = pt2;
          return true;
        }

        pt1.x = p1.x; pt1.y = -1;
        pt2.x = p2.x; pt2.y = -1;
        if (had_0_link(p1, pt1) && had_0_link(p2, pt2)) {
          *o1 = pt1; *o2 = pt2;
          return true;
        }

        pt1.x = p1.x; pt1.y = LINKUP_MAP_HEIGHT;
        pt2.x = p2.x; pt2.y = LINKUP_MAP_HEIGHT;
        if (had_0_link(p1, pt1) && had_0_link(p2, pt2)) {
          *o1 = pt1; *o2 = pt2;
          return true;
        }
      }

      int i;
      //search direction 1
      for(i=p1.y+1;i<LINKUP_MAP_HEIGHT;i++){
        if (!d_in_map(maps_[i][p1.x])){
          Point c1;
          c1.x=p1.x;
          c1.y=i;
          Point d1;
          if (had_1_link(c1,p2,&d1)){
            o1->x=c1.x;
            o1->y=c1.y;
            o2->x=d1.x;
            o2->y=d1.y;
            return true;
          }
        } else break;
      }
      //search direction 2
      for(i=p1.y-1;i>-1;i--){
        if (!d_in_map(maps_[i][p1.x])){
          Point c1;
          c1.x=p1.x;
          c1.y=i;
          Point d1;
          if (had_1_link(c1,p2, &d1)){
            o1->x=c1.x;
            o1->y=c1.y;
            o2->x=d1.x;
            o2->y=d1.y;
            return true;
          }
        } else break;
      }
      //search direction 3
      for(i=p1.x+1;i<LINKUP_MAP_WIDTH;i++){
        if (!d_in_map(maps_[p1.y][i])){
          Point c1;
          c1.x=i;
          c1.y=p1.y;
          Point d1;
          if (had_1_link(c1,p2, &d1)){
            o1->x=c1.x;
            o1->y=c1.y;
            o2->x=d1.x;
            o2->y=d1.y;
            return true;
          }
        } else break;
      }
      //search direction 4
      for(i=p1.x-1;i>-1;i--){
        if (!d_in_map(maps_[p1.y][i])){
          Point c1;
          c1.x=i;
          c1.y=p1.y;
          Point d1;
          if (had_1_link(c1,p2,&d1)){
            o1->x=c1.x;
            o1->y=c1.y;
            o2->x=d1.x;
            o2->y=d1.y;
            return true;
          }
        } else break;
      }
      return false;
    }
  protected:
    void on_check_link() {
      if (last_click_id_ == -1 || cur_click_id_ == -1)
        return;

      Point pt1, pt2;
      pt1.x = cur_click_id_ % LINKUP_MAP_WIDTH;
      pt1.y = cur_click_id_ / LINKUP_MAP_WIDTH;
      pt2.x = last_click_id_ % LINKUP_MAP_WIDTH;
      pt2.y = last_click_id_ / LINKUP_MAP_WIDTH;

      pts_.clear();
      if (maps_[pt1.y][pt1.x] == maps_[pt2.y][pt2.x]) {
        Point out1, out2;
        if (had_0_link(pt1, pt2)) {
          pts_.push_back(pt1);
          pts_.push_back(pt2);
        } else if (had_1_link(pt1, pt2, &out1)) {
          pts_.push_back(pt1);
          pts_.push_back(out1);
          pts_.push_back(pt2);
        } else if (had_2_link(pt1, pt2, &out1, &out2)) {
          pts_.push_back(pt1);
          pts_.push_back(out1);
          pts_.push_back(out2);
          pts_.push_back(pt2);
        }
      }

      on_link();
    }
    void on_link() {
      if (pts_.size() > 0) {
        timer_link_ = add_timer(100);
        play_sound(music_link_ok_);
      } else {
        cur_click_id_ = -1;
        play_sound(music_link_err_);
      }
      invaldate();
    }
    void on_linked() {
      for (auto &p : pts_) {
        if (id_in_map(p.x, p.y))
          maps_[p.y][p.x] = -1;
      }
      pts_.clear();
      last_click_id_ = cur_click_id_ = -1;
      invaldate();
      do_user_event(LINKUP_ID_CHECK_GAMEOVER);
    }
    void on_ctrl_tip(AppControl *ctrl) {
      if (id_in_map(tip_pt1_.x, tip_pt1_.y)
          && id_in_map(tip_pt2_.x, tip_pt2_.y)
          && d_in_map(maps_[tip_pt1_.y][tip_pt1_.x])
          && d_in_map(maps_[tip_pt2_.y][tip_pt2_.x])) {
        return;
      }

      if (tip_times_ == 0) return;
      Point pt1, pt2;
      if (get_one_link(&pt1, &pt2)) {
        tip_pt1_ = pt1;
        tip_pt2_ = pt2;
        --tip_times_;
        invaldate();
      }
    }
  protected:
    std::vector<cvs::c2d::IBitmap*> icons_;
    int maps_[LINKUP_MAP_HEIGHT][LINKUP_MAP_WIDTH];
    int last_click_id_, cur_click_id_;
    std::vector<Point> pts_;

    cvs::c2d::IShapeRect *shape_rect_;
    cvs::c2d::IShapePolyline *shape_polyline_;
    cvs::c2d::ISolidColorBrush *brush_;

    cvs::Color mask_, border_, link_;
    cvs::dtype link_width_;

    SDL_TimerID timer_link_;
    SDL_TimerID timer_toplay_;
    SDL_TimerID timer_time_;

    eLinkupState state_;

    Uint32 time_begin_;
    Uint32 time_end_;

    Mix_Music *music_link_ok_;
    Mix_Music *music_link_err_;

    int level_;

    AppControl *ctrl_tip_;
    int tip_times_;
    Point tip_pt1_, tip_pt2_;
};

int main(int argc, char *argv[]) {
  Linkup app;

  auto rst = app.init();
  if (cvs::Succeeded(rst))
    rst = app.create_window("linkup", cvs::Size(800.f, 600.f), SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE);

  if (cvs::Succeeded(rst))
    app.run(APP_RUN_SLOWEST);

  app.uninit();
  return 0;
}